/*
 * Copyright 2006 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.rebind.rpc;

import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.JPrimitiveType;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.core.ext.typeinfo.TypeOracleException;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;

import java.io.PrintWriter;

/**
 * Creates client-side proxies for remote services.
 */
class ProxyCreator {
  private static final String PROXY_SUFFIX = "_Proxy";
  private static final String SERVICEDEFTARGET_INTF = "com.google.gwt.user.client.rpc.ServiceDefTarget";
  private static final String SERVERCALL_RESPONSE_HANDLER_INTF = "com.google.gwt.user.client.ResponseTextHandler";
  private static final String INVOCATION_TARGET_EXCEPTION_CLASS_NAME = "com.google.gwt.user.client.rpc.InvocationException";
  private static final String SERVICEDEFTARGET_ENTRYPOINT_EXCEPTION = "com.google.gwt.user.client.rpc.ServiceDefTarget.NoServiceEntryPointSpecifiedException";
  private static final String SERIALIZATION_STREAM_CLASS_NAME = "com.google.gwt.user.client.rpc.ClientSerializationStream";
  private static final String SERIALIZATION_STREAM_INSTANTIATION_STATEMENT = SERIALIZATION_STREAM_CLASS_NAME
    + " stream = new " + SERIALIZATION_STREAM_CLASS_NAME + "(SERIALIZER);";

  private SerializableTypeOracle serializableTypeOracle;
  private JClassType serviceIntf;
  private JClassType objectType;
  private boolean enforceTypeVersioning;

  public ProxyCreator(JClassType serviceIntf,
      SerializableTypeOracle serializableTypeOracle) {
    this.serviceIntf = serviceIntf;
    this.serializableTypeOracle = serializableTypeOracle;

    if (serviceIntf.isInterface() == null)
      throw new RuntimeException("Expecting a service interface, but "
        + serviceIntf.getQualifiedSourceName() + " is not an interface");
  }

  private String getPackageName() {
    JPackage pkg = serviceIntf.getPackage();
    if (pkg != null) {
      return pkg.getName();
    }

    return "";
  }

  private String getProxySimpleName() {
    String[] name = Shared.synthesizeTopLevelClassName(serviceIntf,
      PROXY_SUFFIX);
    return name[1];
  }

  private String getProxyQualifiedName() {
    String[] name = Shared.synthesizeTopLevelClassName(serviceIntf,
      PROXY_SUFFIX);
    return name[0].length() == 0 ? name[1] : name[0] + "." + name[1];
  }

  private String getAsyncIntfQualifiedName() {
    String asyncIntf = serviceIntf.getQualifiedSourceName() + "Async";
    return asyncIntf;
  }

  private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx) {
    PrintWriter printWriter = ctx.tryCreate(logger, getPackageName(),
      getProxySimpleName());
    if (printWriter == null) {
      return null;
    }

    ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
      getPackageName(), getProxySimpleName());

    composerFactory.addImport("com.google.gwt.core.client.GWT");
    composerFactory
      .addImport("com.google.gwt.core.client.GWT.UncaughtExceptionHandler");

    composerFactory.addImplementedInterface(SERVICEDEFTARGET_INTF);
    composerFactory.addImplementedInterface(getAsyncIntfQualifiedName());

    return composerFactory.createSourceWriter(ctx, printWriter);
  }

  /**
   * Implements the ServiceDefTarget interface to allow clients to switch which
   * back-end service definition we send calls to.
   */
  private void generateServiceDefTargetImpl(SourceWriter w) {
    String serverDefName = getDefaultServiceDefName();
    if (serverDefName != null) {
      serverDefName = "\"" + serverDefName + "\"";
    } else {
      serverDefName = "null";
    }

    w.println();
    w.println("String fServiceEntryPoint = " + serverDefName + ";");
    w.println();
    w
      .println("public String getServiceEntryPoint() { return fServiceEntryPoint; }");
    w.println();
    w
      .println("public void setServiceEntryPoint(String s) { fServiceEntryPoint = s; }");
  }

  /**
   * Generate the code that addresses the service.
   * 
   * @return
   */
  private boolean generateProxyEncode(GeneratorContext ctx, SourceWriter w,
      SerializableTypeOracle serializableTypeOracle, JClassType serviceIntf,
      JMethod method) {
    boolean isStale = false;
    String serviceIntfName = serviceIntf.getQualifiedSourceName();
    String returnTypeName = method.getReturnType().getQualifiedSourceName();
    String methodName = method.getName();
    JParameter[] params = method.getParameters();
    w.println();
    w.print("private void __" + methodName + "("
      + SERIALIZATION_STREAM_CLASS_NAME + " stream");
    for (int i = 0; i < params.length; i++) {
      JParameter param = params[i];
      w.print(", " + getJavaTypeName(param.getType()) + " " + param.getName());
    }

    w.println(") throws " + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME + " {");
    w.indent();

    // Make sure that we have a service def class name specified.
    //
    w.println("if (getServiceEntryPoint() == null)");
    w.indentln("throw new " + SERVICEDEFTARGET_ENTRYPOINT_EXCEPTION + "();");

    // Generate code to describe just enough meta data for the server to locate
    // the service definition class and resolve the method overload.
    //
    w.println("stream.prepareToWrite();");

    if (!shouldEnforceTypeVersioning()) {
      w.println("stream.addFlags(" + SERIALIZATION_STREAM_CLASS_NAME
        + ".SERIALIZATION_STREAM_FLAGS_NO_TYPE_VERSIONING);");
    }
    w.println("stream.writeObject(\""
      + serializableTypeOracle.getSerializedTypeName(serviceIntf) + "\");");
    w.println("stream.writeObject(\"" + methodName + "\");");
    w.println("stream.writeInt(" + params.length + ");");
    for (int i = 0; i < params.length; ++i) {
      JParameter param = params[i];
      w.println("stream.writeObject(\""
        + serializableTypeOracle.getSerializedTypeName(param.getType())
        + "\");");
    }

    // Encode the arguments.
    //
    for (int i = 0; i < params.length; i++) {
      JParameter param = params[i];
      generateEncodeCall(ctx, w, param);
    }

    w.outdent();
    w.println("}");

    return isStale;
  }

  private boolean shouldEnforceTypeVersioning() {
    return enforceTypeVersioning;
  }

  /*
   * Give a type emit an expression for deserializing that type from a
   * serialization stream.
   */
  protected final void generateDecodeCall(GeneratorContext ctx, SourceWriter w,
      String name, JType type) {
    w.print("stream.");
    JType apiType = getSerializationAPIType(ctx, type);
    w.print("read" + capitalize(apiType.getSimpleSourceName()) + "()");
  }

  /*
   * Give a type emit an expression for serializing that type into a
   * serialization stream.
   */
  protected void generateEncodeCall(GeneratorContext ctx, SourceWriter w,
      JParameter parameter) {
    JType paramType = parameter.getType();
    w.print("stream.");
    JType apiType = getSerializationAPIType(ctx, paramType);
    w.print("write" + capitalize(apiType.getSimpleSourceName()));
    w.println("(" + parameter.getName() + ");");
  }

  /*
   * The system can serialize any type that has been determined to be
   * serializable. However, the API deals only with a distinct set of types.
   * This method takes a type and returns a type that can be passed to the
   * Serialization API.
   */
  private JType getSerializationAPIType(GeneratorContext ctx, JType type) {
    assert (type != null);

    if (type.isPrimitive() != null) {
      return type;
    }

    JParameterizedType parameterizedType = type.isParameterized();
    if (parameterizedType != null) {
      return getSerializationAPIType(ctx, parameterizedType.getRawType());
    }

    TypeOracle typeOracle = ctx.getTypeOracle();
    assert (typeOracle != null);
    if (objectType == null) {
      objectType = typeOracle.findType("java.lang.Object");
    }

    type = objectType;

    return type;
  }

  private static String capitalize(String name) {
    String s = name.substring(0, 1).toUpperCase() + name.substring(1);
    return s;
  }

  /**
   * 
   * @param ctx
   * @param w
   */
  private void generateProxyMethods(GeneratorContext ctx, SourceWriter w) {
    JMethod[] methods = serviceIntf.getMethods();

    for (int index = 0; index < methods.length; ++index) {
      JMethod method = methods[index];
      assert (method != null);

      generateProxyEncode(ctx, w, serializableTypeOracle, serviceIntf, method);

      generateAsynchronousProxyMethod(ctx, w, method);
    }
  }

  /*
   * Calls the __ version to encode.
   */
  private void generateAsynchronousProxyMethod(GeneratorContext ctx,
      SourceWriter w, JMethod method) {
    JType returnType = method.getReturnType();
    String returnTypeName = returnType.getQualifiedSourceName();
    JParameter[] params = method.getParameters();

    w.println();
    w.print("public void " + method.getName() + "(");
    int i;
    for (i = 0; i < params.length; i++) {
      JParameter param = params[i];
      w.print((i > 0 ? ", " : "") + getJavaTypeName(param.getType()) + " "
        + param.getName());
    }

    w.println((i > 0 ? ", final " : "final ") + Shared.ASYNC_CALLBACK_INTERFACE_NAME
      + " callback) {");
    w.indent();
    w.println("final " + SERIALIZATION_STREAM_INSTANTIATION_STATEMENT);
    w.println("try {");
    w.indent();
    {
      w.print("__" + method.getName() + "(stream");
      for (i = 0; i < params.length; i++) {
        w.print(", " + params[i].getName());
      }
      w.println(");");
    }
    w.outdent();
    w
      .println("} catch (" + Shared.SERIALIZATION_EXCEPTION_CLASS_NAME
        + " e) {");
    w.indentln("callback.onFailure(new "
      + INVOCATION_TARGET_EXCEPTION_CLASS_NAME + "(e.getMessage()));");
    w.indentln("return;");
    w.println("}");

    // Generate the async response handler.
    //
    w.println(SERVERCALL_RESPONSE_HANDLER_INTF + " handler = new "
      + SERVERCALL_RESPONSE_HANDLER_INTF + "() {");
    w.indent();
    {
      w.println("public final void onCompletion(String encodedResponse) {");
      w.indent();
      {
        w
          .println("UncaughtExceptionHandler handler = GWT.getUncaughtExceptionHandler();");
        w.println("if (handler != null)");
        w.indent();
        {
          w.println("onCompletionAndCatch(encodedResponse, handler);");
        }
        w.outdent();
        w.println("else");
        w.indent();
        {
          w.println("onCompletionImpl(encodedResponse);");
        }
        w.outdent();
      }
      w.outdent();
      w.println("}");

      w
        .println("private void onCompletionAndCatch(String encodedResponse, UncaughtExceptionHandler handler) {");
      w.indent();
      {
        w.println("try {");
        w.indent();
        {
          w.println("onCompletionImpl(encodedResponse);");
        }
        w.outdent();
        w.println("} catch (Throwable e) {");
        w.indent();
        {
          w.println("handler.onUncaughtException(e);");
        }
        w.outdent();
        w.println("}");
      }
      w.outdent();
      w.println("}");

      w.println("private void onCompletionImpl(String encodedResponse) {");
      w.indent();
      {
        w.println("Object result = null;");
        w.println("Throwable caught = null;");
        w.println("try {");
        w.indent();
        {
          w.println("if (encodedResponse.startsWith(\"{OK}\")) {");
          w.indent();
          {
            w.println("stream.prepareToRead(encodedResponse.substring(4));");
            w.println("stream.readInt(); // read the stream version number");
            w.println("stream.readInt(); // read the stream flags");
            w.print("result = ");

            JPrimitiveType primitive = returnType.isPrimitive();
            if (primitive == JPrimitiveType.VOID) {
              w.print("null");
            } else {
              if (primitive != null) {
                w.print("new ");
                w.print(getObjectWrapperName(primitive));
                w.print("(");
                generateDecodeCall(ctx, w, null, returnType);
                w.print(")");
              } else {
                generateDecodeCall(ctx, w, null, returnType);
              }
            }
            w.println(";");
          }
          w.outdent();
          w.println("} else if (encodedResponse.startsWith(\"{EX}\")) {");
          w.indent();
          {
            w.println("stream.prepareToRead(encodedResponse.substring(4));");
            w.println("stream.readInt(); // read the stream version number");
            w.println("stream.readInt(); // read the stream flags");
            w.println("caught = (Throwable) stream.readObject();");
          }
          w.outdent();
          w.println("} else {");
          w.indent();
          {
            w.println("caught = new " + INVOCATION_TARGET_EXCEPTION_CLASS_NAME
              + "(encodedResponse);");
          }
          w.outdent();
          w.println("}");
        }
        w.outdent();
        w.println("} catch (Throwable e) {");
        w.indent();
        {
          w.println("caught = e;");
        }
        w.outdent();
        w.println("}");

        w.println("if (caught == null)");
        w.indent();
        {
          w.println("callback.onSuccess(result);");
        }
        w.outdent();
        w.println("else");
        w.indent();
        {
          w.println("callback.onFailure(caught);");
        }
        w.outdent();
      }
      w.outdent();
      w.println("}");
    }
    w.outdent();
    w.println("};");

    // Make the asynchronous invocation.
    //
    w
      .println("if (!com.google.gwt.user.client.HTTPRequest.asyncPost(getServiceEntryPoint(), stream.toString(), handler))");
    w
      .indentln("callback.onFailure(new "
        + INVOCATION_TARGET_EXCEPTION_CLASS_NAME
        + "(\"Unable to initiate the asynchronous service invocation -- check the network connection\"));");
    w.outdent();

    w.println("}");
  }

  /*
   * Determine the name of the object wrapper class to instantiate based on the
   * the type of the primitive.
   */
  private String getObjectWrapperName(JPrimitiveType primitive) {
    if (primitive == JPrimitiveType.INT) {
      return "Integer";
    } else if (primitive == JPrimitiveType.CHAR) {
      return "Character";
    }

    return capitalize(primitive.getSimpleSourceName());
  }

  /*
   * This method returns the real type name. Currently, it only affects
   * JParameterizedType since their names are not legal Java names.
   */
  private static String getJavaTypeName(JType type) {
    JParameterizedType parameterizedType = type.isParameterized();
    if (parameterizedType != null) {
      return parameterizedType.getRawType().getQualifiedSourceName();
    }

    return type.getQualifiedSourceName();
  }

  /*
   * Return an expression to cast from a normalized type expression to the
   * non-normalized type.
   */
  private String generateCastExpression(GeneratorContext ctx, JType type) {
    String castExpr;

    if (type != getSerializationAPIType(ctx, type)) {
      castExpr = "(" + getJavaTypeName(type) + ") ";
    } else {
      castExpr = "";
    }

    return castExpr;
  }

  /**
   * 
   */
  private String getDefaultServiceDefName() {
    String[][] metaData = serviceIntf
      .getMetaData(Shared.ENTRY_POINT_TAG);
    if (metaData.length == 0) {
      return null;
    }
    return serviceIntf.getMetaData(Shared.ENTRY_POINT_TAG)[0][0];
  }

  /**
   * Creates a proxy class for the requested class.
   */
  public String create(TreeLogger logger, GeneratorContext context) {
    assert (isValidServiceInterface(logger, context));

    logger = logger.branch(TreeLogger.SPAM,
      "Generating RPC Proxy for service interface '"
        + serviceIntf.getQualifiedSourceName() + "'", null);
    SourceWriter srcWriter = getSourceWriter(logger, context);
    if (srcWriter == null) {
      return getProxyQualifiedName();
    }

    initializeProperties(logger, context);

    generateProxyFields(srcWriter);

    generateServiceDefTargetImpl(srcWriter);

    generateProxyMethods(context, srcWriter);

    srcWriter.commit(logger);

    return getProxyQualifiedName();
  }

  private void initializeProperties(TreeLogger logger, GeneratorContext context) {
    PropertyOracle propertyOracle = context.getPropertyOracle();
    try {
      String propVal = propertyOracle.getPropertyValue(logger,
        Shared.RPC_PROP_ENFORCE_TYPE_VERSIONING);
      if (propVal != null && propVal.equals("false")) {
        enforceTypeVersioning = false;
      } else {
        enforceTypeVersioning = true;
      }

      return;
    } catch (BadPropertyValueException e) {
      // Purposely ignored, because we want to enforce RPC versioning if
      // the property is not defined
      //
    }

    enforceTypeVersioning = true;
  }

  /**
   * Generate any fields required by the proxy.
   */
  private void generateProxyFields(SourceWriter srcWriter) {
    String typeSerializerName = serializableTypeOracle
      .getTypeSerializerQualifiedName(serviceIntf);
    srcWriter.println("private static final " + typeSerializerName
      + " SERIALIZER = new " + typeSerializerName + "();");
  }

  /**
   * This is really a test method that allows us to assert at the start of the
   * create method that we are in fact dealing with a valid service interface.
   * If you are running with assertions diabled, this code should not be called.
   */
  private boolean isValidServiceInterface(TreeLogger logger,
      GeneratorContext ctx) {
    assert (serviceIntf != null);
    assert (serializableTypeOracle != null);

    ServiceInterfaceValidator siv = new ServiceInterfaceValidator(logger, ctx,
      serializableTypeOracle, serviceIntf);

    try {
      return siv.isValid();
    } catch (TypeOracleException e) {
      logger.branch(TreeLogger.ERROR, "TypeOracleException: ", e);
      // Purposely ignored
      return false;
    }
  }
}
